package com.tools.web.work.captcha;

import com.tools.common.io.FileKit;
import com.tools.common.io.IOKit;
import com.tools.common.object.Note;
import com.wf.captcha.*;
import com.wf.captcha.base.Captcha;
import javax.servlet.http.HttpServletResponse;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 验证码工具类
 * */
@Note("验证码工具类")
public final class CaptchaKit {

    private CaptchaKit() {}

    @Note("如果是使用点选文字的验证码，那么文字模板字符串就会被初始化")
    private static String CHINESE_WORD;

    @Note("如果是使用点选文字的验证码，那么生成文字的颜色就会被初始化")
    private static Color[] COLORS;

    public static String getChineseWord() {
        return CHINESE_WORD;
    }

    public static Color[] getColors() {
        return COLORS;
    }

    public synchronized void setColors(Color[] colors) {
        COLORS = colors;
    }


    @Note("工厂模式获取 PNG 格式的验证码")
    public static SpecCaptcha png() {
        SpecCaptcha captcha = new SpecCaptcha();
        captcha.setCharType(Captcha.TYPE_DEFAULT);
        return captcha;
    }


    @Note("工厂模式获取指定宽高、验证码长度的 PNG 格式的验证码")
    public static SpecCaptcha png(CaptchaBuilder builder) {
        if(builder == null) throw new NullPointerException("验证码构建对象 CaptchaBuilder 为 null");
        SpecCaptcha captcha = new SpecCaptcha(builder.getWidth(), builder.getHeight(), builder.getLength());
        captcha.setCharType(Captcha.TYPE_DEFAULT);
        return captcha;
    }


    @Note("工厂模式获取 GIF 动态图片的验证码")
    public static GifCaptcha gif() {
        GifCaptcha captcha = new GifCaptcha();
        captcha.setCharType(Captcha.TYPE_DEFAULT);
        return captcha;
    }


    @Note("工厂模式获取指定宽高、验证码长度的 GIF 动态图片的验证码")
    public static GifCaptcha gif(CaptchaBuilder builder) {
        if(builder == null) throw new NullPointerException("验证码构建对象 CaptchaBuilder 为 null");
        GifCaptcha gifCaptcha = new GifCaptcha(builder.getWidth(), builder.getHeight(), builder.getLength());
        gifCaptcha.setCharType(Captcha.TYPE_DEFAULT);
        return gifCaptcha;
    }


    @Note("工厂模式获取中文的静态图片验证码")
    public static ChineseCaptcha chinese() {
        ChineseCaptcha captcha = new ChineseCaptcha();
        captcha.setCharType(Captcha.TYPE_DEFAULT);
        return captcha;
    }


    @Note("工厂模式获取指定宽高、验证码长度的中文的静态图片验证码")
    public static ChineseCaptcha chinese(CaptchaBuilder builder) {
        if(builder == null) throw new NullPointerException("验证码构建对象 CaptchaBuilder 为 null");
        ChineseCaptcha captcha = new ChineseCaptcha(builder.getWidth(), builder.getHeight(), builder.getLength());
        captcha.setCharType(Captcha.TYPE_DEFAULT);
        return captcha;
    }


    @Note("工厂模式获取 GIF 格式中文的动态图片验证码")
    public static ChineseGifCaptcha chineseGif() {
        ChineseGifCaptcha captcha = new ChineseGifCaptcha();
        captcha.setCharType(Captcha.TYPE_DEFAULT);
        return captcha;
    }


    @Note("工厂模式获取指定宽高、验证码长度的 GIF 格式中文的动态图片验证码")
    public static ChineseGifCaptcha chineseGif(CaptchaBuilder builder) {
        if(builder == null) throw new NullPointerException("验证码构建对象 CaptchaBuilder 为 null");
        ChineseGifCaptcha captcha = new ChineseGifCaptcha(builder.getWidth(), builder.getHeight(), builder.getLength());
        captcha.setCharType(Captcha.TYPE_DEFAULT);
        return captcha;
    }


    @Note("工厂模式获取算术类型的图片验证码")
    public static ArithmeticCaptcha calc() {
        return new ArithmeticCaptcha();
    }


    @Note("工厂模式获取指定宽高、验证码长度算术类型的图片验证码")
    public static ArithmeticCaptcha calc(CaptchaBuilder builder) {
        if(builder == null) throw new NullPointerException("验证码构建对象 CaptchaBuilder 为 null");
        return new ArithmeticCaptcha(builder.getWidth(), builder.getHeight(), builder.getLength());
    }


    @Note("设置响应验证码图片的一些响应头信息")
    public static void setResponseHeader(HttpServletResponse response, String contentType) {
        response.setContentType(contentType);
        response.setHeader("Pragma", "No-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0L);
    }

    @Note("返回滑轮验证码的字节流 Base64 编码的字符串、以及阻塞块的坐标值等信息")
    public static SlideCaptcha slide(SlideCaptchaBuilder builder) {
        if(builder == null) throw new NullPointerException("滑动验证图的构建实例为 null");
        BufferedImage srcImage = builder.getSrcImage();
        //获取画布的宽高
        int canvasWidth = builder.getImageWidth();
        int canvasHeight = builder.getImageHeight();
        //获取阻塞块的宽高/半径
        int blockWidth = builder.getBlockWidth();
        if(blockWidth <= 0) blockWidth = (int) (canvasWidth / 4.9);
        int blockHeight = builder.getBlockHeight();
        if(blockHeight <= 0) blockHeight = (int) (canvasHeight / 2.8);
        int blockRadius = builder.getBlockRadius();
        if(blockRadius <= 0) blockRadius = (int) (blockWidth / 7.2);
        //调整原图到指定大小
        srcImage = private_imageResize(srcImage, canvasWidth, canvasHeight);
        //随机生成阻塞块坐标

        int blockX = private_getRandomNumber(blockWidth, canvasWidth - blockWidth - 10);
        int blockY = private_getRandomNumber(10, canvasHeight - blockHeight + 1);

        //生成阻塞块图片
        BufferedImage blockImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
        //新建的图像根据轮廓图颜色赋值，源图生成遮罩
        private_cutByTemplate(srcImage, blockImage, blockWidth, blockHeight, blockRadius, blockX, blockY);
        //设置返回参数
        SlideCaptcha result = new SlideCaptcha(blockX, blockY, builder.getFaultTolerant());
        result.setBlockImage(IOKit.readToBase64ForImg(blockImage, builder.getBlockSuffix()));
        result.setCanvasImage(IOKit.readToBase64ForImg(srcImage, builder.getSrcImageSuffix()));
        return result;
    }



    @Note("生成点选文字的验证码并返回")
    public static ClickCaptcha clickSelect(ClickCaptchaBuilder builder) {
        if(builder == null) throw new NullPointerException("点选文字的验证图的构建实例为 null");
        private_lazyLoadClickCaptchaTemplate();
        BufferedImage background = builder.getBackground();
        int backgroundWidth = builder.getBackgroundWidth();
        int backgroundHeight = builder.getBackgroundHeight();
        Graphics2D g = (Graphics2D) background.getGraphics();
        Font font = new Font(builder.getFontStyle(), Font.PLAIN, private_calcFontSize(backgroundWidth, backgroundHeight));
        g.setFont(font);
        g.setBackground(Color.WHITE);
        //画边框
        g.drawRect(0, 0, backgroundWidth - 1, backgroundHeight - 1);
        ThreadLocalRandom random = ThreadLocalRandom.current();
        int length = CHINESE_WORD.length();
        int numberOfWord = builder.getNumberOfWord();

        //生成文字，让客户端按数组的这个存放的顺序依次点击
        ClickChar[] chars = new ClickChar[numberOfWord];
        int index = 0;
        int fontSize = font.getSize();
        int xBound = backgroundWidth - fontSize;
        int yBound = backgroundHeight - fontSize;
        int faultTolerant = builder.getFaultTolerant();
        while (index  < numberOfWord) {
            char c = CHINESE_WORD.charAt(random.nextInt(length));
            int x = random.nextInt(xBound);
            int y = random.nextInt(yBound);
            if(x == y) continue;
            if(y < fontSize) y += fontSize;
            chars[index] = new ClickChar(c, x, y, faultTolerant);
            index++;
        }

        //按照乱序将文字画到背景图上
        int len = COLORS.length;
        int residue = numberOfWord;
        while (residue > 0) {
            int i = random.nextInt(numberOfWord);
            ClickChar cc = chars[i];
            if(cc != null) {
                g.setColor(COLORS[random.nextInt(len)]);
                g.drawString(String.valueOf(cc.getWord()), cc.getX(), cc.getY());
                chars[i] = null;
                residue--;
            }
        }
        //释放资源并返回图片的 Base64 编码和文字包装实例数组
        g.dispose();
        return new ClickCaptcha(IOKit.readToBase64ForImg(background, builder.getImageSuffix()), chars);
    }

    /* ********************************************************************************************
     *
     *          生成 CaptchaBean 实例
     *
     * *********************************************************************************************
     * */

    public static CaptchaResult pngCaptchaResult() {
        SpecCaptcha png = png();
        return new CaptchaResult(png);
    }

    public static CaptchaResult pngCaptchaResult(CaptchaBuilder builder) {
        SpecCaptcha png = png(builder);
        return new CaptchaResult(png, builder.getAllowErrorCount());
    }

    public static CaptchaResult gifCaptchaResult() {
        GifCaptcha gif = gif();
        return new CaptchaResult(gif);
    }

    public static CaptchaResult gifCaptchaResult(CaptchaBuilder builder) {
        GifCaptcha gif = gif(builder);
        return new CaptchaResult(gif, builder.getAllowErrorCount());
    }
    
    public static CaptchaResult chineseCaptchaResult() {
        ChineseCaptcha chinese = chinese();
        return new CaptchaResult(chinese);
    }

    public static CaptchaResult chineseCaptchaResult(CaptchaBuilder builder) {
        ChineseCaptcha chinese = chinese(builder);
        return new CaptchaResult(chinese, builder.getAllowErrorCount());
    }

    public static CaptchaResult chineseGifCaptchaResult() {
        ChineseGifCaptcha chineseGif = chineseGif();
        return new CaptchaResult(chineseGif);
    }

    public static CaptchaResult chineseGifCaptchaResult(CaptchaBuilder builder) {
        ChineseGifCaptcha chineseGif = chineseGif(builder);
        return new CaptchaResult(chineseGif, builder.getAllowErrorCount());
    }
    
    public static CaptchaResult calcCaptchaResult() {
        ArithmeticCaptcha arithmetic = calc();
        return new CaptchaResult(arithmetic);
    }

    public static CaptchaResult calcCaptchaResult(CaptchaBuilder builder) {
        ArithmeticCaptcha arithmetic = calc(builder);
        return new CaptchaResult(arithmetic, builder.getAllowErrorCount());
    }

    public static CaptchaResult slideCaptchaResult(SlideCaptchaBuilder builder) {
        SlideCaptcha slide = slide(builder);
        return new CaptchaResult(slide, builder.getAllowErrorCount());
    }

    public static CaptchaResult clickSelectCaptchaResult(ClickCaptchaBuilder builder) {
        ClickCaptcha clickSelect = clickSelect(builder);
        return new CaptchaResult(clickSelect, builder.getAllowErrorCount());
    }

    /* ********************************************************************************************
     *
     *          私有逻辑
     *
     * *********************************************************************************************
     * */

    @Note("检查生成的点选文字的大小")
    private static boolean private_checkIsAddChar(ClickChar[] chars, int length, int x, int y, int fontSize) {
        for (int i = 0; i < length; i++) {
            ClickChar cc = chars[i];
            if(Math.abs(cc.getX() - x) <= fontSize) return false;
            if(Math.abs(cc.getY() - y) <= fontSize) return false;
        }
        return true;
    }


    @Note("获取一个随机数")
    private static int private_getRandomNumber(int start, int end) {
        return ThreadLocalRandom.current().nextInt(end - start + 1) + start;
    }

    @Note("如果背景画布的大小为负数，则采用原图的大小，不重新创建一个新的图片")
    private static BufferedImage private_imageResize(BufferedImage srcImage, int canvasWidth, int canvasHeight) {
        if(canvasWidth < 0 && canvasHeight < 0) return srcImage;
        if(canvasWidth < 0) canvasWidth = srcImage.getWidth();
        if(canvasHeight < 0) canvasHeight = srcImage.getHeight();
        Image image = srcImage.getScaledInstance(canvasWidth, canvasHeight, Image.SCALE_SMOOTH);
        BufferedImage resultImage = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics2D = resultImage.createGraphics();
        graphics2D.drawImage(image, 0, 0, null);
        graphics2D.dispose();
        return resultImage;
    }


    @Note("抠出阻塞块的图，并生成阻塞块")
    private static void private_cutByTemplate(BufferedImage srcImage, BufferedImage blockImage, int blockWidth, int blockHeight, int blockRadius, int blockX, int blockY) {
        BufferedImage waterImage = new BufferedImage(blockWidth, blockHeight, BufferedImage.TYPE_4BYTE_ABGR);
        //阻塞块的轮廓图
        int[][] blockData = private_getBlockData(blockWidth, blockHeight, blockRadius);
        //创建阻塞块具体形状
        for (int i = 0; i < blockWidth; i++) {
            for (int j = 0; j < blockHeight; j++) {
                try {
                    //原图中对应位置变色处理
                    if (blockData[i][j] == 1) {
                        //背景设置为黑色
                        waterImage.setRGB(i, j, Color.BLACK.getRGB());
                        blockImage.setRGB(i, j, srcImage.getRGB(blockX + i, blockY + j));
                        //轮廓设置为白色，取带像素和无像素的界点，判断该点是不是临界轮廓点
                        if (blockData[i + 1][j] == 0 || blockData[i][j + 1] == 0 || blockData[i - 1][j] == 0 || blockData[i][j - 1] == 0) {
                            blockImage.setRGB(i, j, Color.WHITE.getRGB());
                            waterImage.setRGB(i, j, Color.WHITE.getRGB());
                        }
                    }
                    //这里把背景设为透明
                    else {
                        blockImage.setRGB(i, j, Color.TRANSLUCENT);
                        waterImage.setRGB(i, j, Color.TRANSLUCENT);
                    }
                } catch (ArrayIndexOutOfBoundsException e) {
                    //防止数组下标越界异常
                }
            }
        }
        //在画布上添加阻塞块水印
        private_addBlockWatermark(srcImage, waterImage, blockX, blockY);
    }


    @Note("构建拼图轮廓轨迹")
    private static int[][] private_getBlockData(int blockWidth, int blockHeight, int blockRadius) {
        int[][] data = new int[blockWidth][blockHeight];
        double po = Math.pow(blockRadius, 2);
        //随机生成两个圆的坐标，在4个方向上 随机找到2个方向添加凸/凹
        //凸/凹1
        int face1 = private_getRandomNumber(0, 4);
        //凸/凹2
        int face2;
        //保证两个凸/凹不在同一位置
        do {
            face2 = private_getRandomNumber(0, 4);
        } while (face1 == face2);
        //获取凸/凹起位置坐标
        int[] circle1 = private_getCircleCoords(face1, blockWidth, blockHeight, blockRadius);
        int[] circle2 = private_getCircleCoords(face2, blockWidth, blockHeight, blockRadius);
        //随机凸/凹类型
        int shape = private_getRandomNumber(0, 1);
        //圆的标准方程 (x-a)²+(y-b)²=r²,标识圆心（a,b）,半径为r的圆
        //计算需要的小图轮廓，用二维数组来表示，二维数组有两张值，0和1，其中0表示没有颜色，1有颜色
        for (int i = 0; i < blockWidth; i++) {
            for (int j = 0; j < blockHeight; j++) {
                data[i][j] = 0;
                //创建中间的方形区域
                if ((i >= blockRadius && i <= blockWidth - blockRadius && j >= blockRadius && j <= blockHeight - blockRadius)) {
                    data[i][j] = 1;
                }
                double d1 = Math.pow(i - Objects.requireNonNull(circle1)[0], 2) + Math.pow(j - circle1[1], 2);
                double d2 = Math.pow(i - Objects.requireNonNull(circle2)[0], 2) + Math.pow(j - circle2[1], 2);
                //创建两个凸/凹
                if (d1 <= po || d2 <= po) {
                    data[i][j] = shape;
                }
            }
        }
        return data;
    }


    @Note("根据朝向获取圆心坐标")
    private static int[] private_getCircleCoords(int face, int blockWidth, int blockHeight, int blockRadius) {
        //上
        if (0 == face) {
            return new int[]{blockWidth / 2 - 1, blockRadius};
        }
        //左
        else if (1 == face) {
            return new int[]{blockRadius, blockHeight / 2 - 1};
        }
        //下
        else if (2 == face) {
            return new int[]{blockWidth / 2 - 1, blockHeight - blockRadius - 1};
        }
        //右
        else if (3 == face) {
            return new int[]{blockWidth - blockRadius - 1, blockHeight / 2 - 1};
        }
        return null;
    }


    @Note("在画布上添加阻塞块水印")
    private static void private_addBlockWatermark(BufferedImage canvasImage, BufferedImage blockImage, int x, int y) {
        Graphics2D graphics2D = canvasImage.createGraphics();
        graphics2D.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, 0.8f));
        graphics2D.drawImage(blockImage, x, y, null);
        graphics2D.dispose();
    }

    @Note("初始化点选文字的验证码字符串模板，从外部文件中读取")
    private static void private_lazyLoadClickCaptchaTemplate() {
        if(CHINESE_WORD != null) return;
        synchronized (CaptchaKit.class) {
            if(CHINESE_WORD != null) return;
            InputStream stream = FileKit.getResourceInputStream("custom/ChineseWord.txt");
            CHINESE_WORD = IOKit.readToString(stream);
            if(COLORS != null && COLORS.length != 0) return;
            COLORS = new Color[] {
                    Color.ORANGE, Color.CYAN, Color.MAGENTA, Color.RED, Color.GREEN,Color.YELLOW
            };
        }
    }


    @Note("根据图片的宽高计算得到字体的最佳大小，取二者各自商的最小值返回")
    private static int private_calcFontSize(int width, int height) {
        return Math.min((int) (width / 9.3), (int) (height / 9.3));
    }
}
